/*
 * PS3 Media Server, for streaming any medias to your PS3.
 * Copyright (C) 2008  A.Brochard
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; version 2
 * of the License only.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package net.pms.network;

import net.pms.PMS;
import net.pms.configuration.PmsConfiguration;
import org.apache.commons.lang3.StringUtils;

import io.netty.bootstrap.AbstractBootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;

import java.io.IOException;
import java.net.*;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ServerSocketChannel;
import java.util.concurrent.Executors;

public class HTTPServer implements Runnable {
	private static final Logger logger = LoggerFactory.getLogger(HTTPServer.class);
	private static final PmsConfiguration configuration = PMS.getConfiguration();
	private final int port;
	private String hostname;
	private ServerSocketChannel serverSocketChannel;
	private ServerSocket serverSocket;
	private boolean stop;
	private Thread runnable;
	private InetAddress iafinal;
	private EventLoopGroup parentroup;
	private Channel channel;
	private NetworkInterface networkInterface;
	private EventLoopGroup group;
	AbstractBootstrap bootstrap;
	protected ChannelInitializer<Channel> handler;
	@Deprecated
	public InetAddress getIafinal() {
		return iafinal;
	}

	public NetworkInterface getNetworkInterface() {
		return networkInterface;
	}

	// use getNetworkInterface()
	@Deprecated
	public NetworkInterface getNi() {
		return getNetworkInterface();
	}

	public HTTPServer(int port) {
		this.port = port;
	}

	public String getURL() {
		return "http://" + hostname + ":" + port;
	}

	public String getHost() {
		return hostname;
	}

	public int getPort() {
		return port;
	}

	public boolean start() throws Exception{
		hostname = configuration.getServerHostname();
		InetSocketAddress address;

		if (StringUtils.isNotBlank(hostname)) {
			logger.debug("Using forced address " + hostname);
			InetAddress tempIA = InetAddress.getByName(hostname);

			if (tempIA != null && networkInterface != null && networkInterface.equals(NetworkInterface.getByInetAddress(tempIA))) {
				address = new InetSocketAddress(tempIA, port);
			} else {
				address = new InetSocketAddress(hostname, port);
			}
		} else if (isAddressFromInterfaceFound(configuration.getNetworkInterface())) { // XXX sets iafinal and networkInterface
			logger.debug("Using address {} found on network interface: {}", iafinal, networkInterface.toString().trim().replace('\n', ' '));
			address = new InetSocketAddress(iafinal, port);
		} else {
			logger.debug("Using localhost address");
			address = new InetSocketAddress(port);
		}

		logger.debug("Created socket: " + address);

		if (configuration.isHTTPEngineV2()) { // HTTP Engine V2
			group = new NioEventLoopGroup();
			bootstrap = new ServerBootstrap();
			parentroup = new NioEventLoopGroup();
			handler = new ChannelInitializer<Channel>() {
				@Override
				protected void initChannel(Channel ch) throws Exception {
					ChannelPipeline p = ch.pipeline();			        
			        /*
			        if (ssl) {
			            SSLEngine engine =
			                SecureChatSslContextFactory.getClientContext().createSSLEngine();
			            engine.setUseClientMode(true);

			            p.addLast("ssl", new SslHandler(engine));
			        }
			         */
			        p.addLast("codec", new HttpClientCodec());

			        // Remove the following line if you don't want automatic content decompression.
			       // p.addLast("inflater", new HttpContentDecompressor());

			        // Uncomment the following line if you don't want to handle HttpChunks.
			        p.addLast("aggregator", new HttpObjectAggregator(1048576));			        
				}
			};
			
			((ServerBootstrap) bootstrap).group(parentroup, group)
					.channel(NioServerSocketChannel.class)
					.option(ChannelOption.TCP_NODELAY, true)
					.option(ChannelOption.AUTO_READ, true)
					.childHandler(handler);			
			

			bootstrap.bind(address).await();

			if (hostname == null && iafinal != null) {
				hostname = iafinal.getHostAddress();
			} else if (hostname == null) {
				hostname = InetAddress.getLocalHost().getHostAddress();
			}
		} else { // HTTP Engine V1
			serverSocketChannel = ServerSocketChannel.open();

			serverSocket = serverSocketChannel.socket();
			serverSocket.setReuseAddress(true);
			serverSocket.bind(address);

			if (hostname == null && iafinal != null) {
				hostname = iafinal.getHostAddress();
			} else if (hostname == null) {
				hostname = InetAddress.getLocalHost().getHostAddress();
			}

			runnable = new Thread(this, "HTTP Server");
			runnable.setDaemon(false);
			runnable.start();
		}

		return true;
	}

	// XXX this sets iafinal and networkInterface
	private boolean isAddressFromInterfaceFound(String networkInterfaceName) {
		NetworkConfiguration.InterfaceAssociation ia = StringUtils.isNotEmpty(networkInterfaceName) ?
			NetworkConfiguration.getInstance().getAddressForNetworkInterfaceName(networkInterfaceName) :
			null;

		if (ia == null) {
			ia = NetworkConfiguration.getInstance().getDefaultNetworkInterfaceAddress();
		}

		if (ia != null) {
			iafinal = ia.getAddr();
			networkInterface = ia.getIface();
		}

		return ia != null;
	}

	// http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=10689&p=48811#p48811
	//
	// avoid a NPE when a) switching HTTP Engine versions and b) restarting the HTTP server
	// by cleaning up based on what's in use (not null) rather than the config state, which
	// might be inconsistent.
	//
	// NOTE: there's little in the way of cleanup to do here as PMS.reset() discards the old
	// server and creates a new one
	public void stop() {
		logger.info("Stopping server on host {} and port {}...", hostname, port);

		if (runnable != null) { // HTTP Engine V1
			runnable.interrupt();
		}

		if (serverSocket != null) { // HTTP Engine V1
			try {
				serverSocket.close();
				serverSocketChannel.close();
			} catch (IOException e) {
				logger.debug("Caught exception", e);
			}
		}

		if (channel != null) { // HTTP Engine V2
			if (group != null) {
				group.shutdownGracefully().awaitUninterruptibly();
			}
		}

		NetworkConfiguration.forgetConfiguration();
	}

	// XXX only used by HTTP Engine V1
	@Override
	public void run() {
		logger.info("Starting DLNA Server on host {} and port {}...", hostname, port);

		while (!stop) {
			try {
				Socket socket = serverSocket.accept();
				InetAddress inetAddress = socket.getInetAddress();
				String ip = inetAddress.getHostAddress();
				// basic IP filter: solntcev at gmail dot com
				boolean ignore = false;

				if (configuration.getIpFiltering().allowed(inetAddress)) {
					logger.trace("Receiving a request from: " + ip);
				} else {
					ignore = true;
					socket.close();
					logger.trace("Ignoring request from: " + ip);
				}

				if (!ignore) {
					RequestHandler request = new RequestHandler(socket);
					Thread thread = new Thread(request, "Request Handler");
					thread.start();
				}
			} catch (ClosedByInterruptException e) {
				stop = true;
			} catch (IOException e) {
				logger.debug("Caught exception", e);
			} finally {
				try {
					if (stop && serverSocket != null) {
						serverSocket.close();
					}

					if (stop && serverSocketChannel != null) {
						serverSocketChannel.close();
					}
				} catch (IOException e) {
					logger.debug("Caught exception", e);
				}
			}
		}
	}
}
